use qjs_sys as q;
use std::collections::{HashMap};
use std::ffi::{CString};

pub struct AutoDropJSValue {
  ctx:*mut q::JSContext,
  raw_value:RawJsValue,
}
impl AutoDropJSValue {
  pub fn new(val:q::JSValue,ctx:*mut q::JSContext) -> Self {
    AutoDropJSValue {
      ctx,
      raw_value:RawJsValue(val)
    }
  }

  pub fn inner(&self) -> RawJsValue {
    self.raw_value
  }

  pub fn drop_js_value(value:q::JSValue,ctx:*mut q::JSContext) {
    if value.tag < 0 {
      unsafe {
        let ptr = value.u.ptr as *mut q::JSRefCountHeader;
        let pref: &mut q::JSRefCountHeader = &mut *ptr;
        pref.ref_count -= 1;
        if pref.ref_count <= 0 {
          //println!("drop CJSValue:{}",pref.ref_count);
          q::__JS_FreeValue(ctx,value); 
        }
      }
    }
  }

  pub fn free_value_rt(rt:*mut q::JSRuntime,value:q::JSValue) {
    if value.tag < 0 {
      unsafe {
        let ptr = value.u.ptr as *mut q::JSRefCountHeader;
        let pref: &mut q::JSRefCountHeader = &mut *ptr;
        if pref.ref_count > 0 {
          pref.ref_count -= 1;
        }
        if pref.ref_count <= 0 {
          let is_live = q::JS_IsLiveObject(rt,value);
          if is_live != 0 {
            q::__JS_FreeValueRT(rt,value); 
          }
        }
      }
    }
  }
}

impl Drop for AutoDropJSValue {
  fn drop(&mut self) {
    AutoDropJSValue::drop_js_value(self.raw_value.0, self.ctx);
  }
}

#[derive(Copy,Clone)]
pub struct RawJsValue(pub q::JSValue);


impl RawJsValue {
  pub fn tag(&self) -> i64 {
    self.0.tag
  }

  pub fn to_value(&self,ctx:*mut q::JSContext) -> Result<JSValue,String> {
    RawJsValue::deserialize_value(self.0,ctx)
  }
   
  pub fn deserialize_value_(c_js_value:q::JSValue,c_ctx:*mut q::JSContext,one_level:bool) -> Result<JSValue,String> {
    let tag:i32 = c_js_value.tag as i32;
    match tag {
      q::JS_TAG_INT  => Ok(unsafe{ JSValue::Int(c_js_value.u.int32) }),
      q::JS_TAG_BOOL => Ok(unsafe{ JSValue::Bool(c_js_value.u.int32 > 0)}),
      q::JS_TAG_NULL => Ok(JSValue::Null),
      q::JS_TAG_UNDEFINED => Ok(JSValue::Undefined),
      q::JS_TAG_FLOAT64 => Ok(JSValue::Float(unsafe{c_js_value.u.float64})),
      q::JS_TAG_STRING => {
        let ptr = unsafe { q::JS_ToCStringLen2(c_ctx, std::ptr::null::<usize>() as *mut usize,c_js_value, 0) };
        if ptr.is_null() { return Err(String::from("String is Null")); }
        let cstr = unsafe { std::ffi::CStr::from_ptr(ptr) };
        let rstr = cstr.to_str().map(|str| str.to_string() );
        unsafe { q::JS_FreeCString(c_ctx, ptr) };
        if rstr.is_ok() { Ok(JSValue::String(rstr.unwrap())) } else { Err(String::from("String Error")) }
      },
      q::JS_TAG_OBJECT => {
        if unsafe { q::JS_IsFunction(c_ctx,c_js_value) > 0 } {
          return Ok(JSValue::Function(AutoDropJSValue::new(c_js_value, c_ctx) ));
        };
        let is_array = unsafe { q::JS_IsArray(c_ctx, c_js_value) } > 0;
        if is_array {
          let lenstring = CString::new("length").unwrap();
          let len_raw = unsafe { q::JS_GetPropertyStr(c_ctx, c_js_value, lenstring.as_ptr()) };
          let js_arr_len = RawJsValue::deserialize_value(len_raw,c_ctx)?;
          let arr_len = match js_arr_len {
            JSValue::Int(len) => len,
            _ => return Err(String::from("array length not found"))
          };
          if one_level {
            let mut values:Vec<AutoDropJSValue> = Vec::new();
            for index in 0..(arr_len as usize) {
              let value_raw = unsafe { q::JS_GetPropertyUint32(c_ctx, c_js_value, index as u32) };
              if value_raw.tag == q::JS_TAG_EXCEPTION.into() {
               return Err(String::from("Could not build array"));
              }
              values.push(AutoDropJSValue::new(value_raw,c_ctx));
            }
            return Ok(JSValue::ArrayRaw(values));
          } else {
            let mut values:Vec<JSValue> = Vec::new();
            for index in 0..(arr_len as usize) {
              let value_raw = unsafe { q::JS_GetPropertyUint32(c_ctx, c_js_value, index as u32) };
              if value_raw.tag == q::JS_TAG_EXCEPTION.into() {
               return Err(String::from("Could not build array"));
              }
              let arr_val = RawJsValue::deserialize_value(value_raw,c_ctx)?;
              values.push(arr_val)
            }
            return Ok(JSValue::Array(values));
          }
          
        } else {
          let mut properties: *mut q::JSPropertyEnum = std::ptr::null_mut();
          let mut count: u32 = 0;
          let flags = (q::JS_GPN_STRING_MASK | q::JS_GPN_ENUM_ONLY ) as i32;
          let ret = unsafe { q::JS_GetOwnPropertyNames(c_ctx, &mut properties, &mut count, c_js_value, flags) };
          if ret != 0 { return Err(String::from("get object property error")); }
          let mut map:HashMap<String,AutoDropJSValue> = HashMap::new();
          for index in 0..count {
            let prop = unsafe { properties.offset(index as isize) };
            let raw_value = unsafe { q::JS_GetPropertyInternal(c_ctx, c_js_value, (*prop).atom, c_js_value, 0) };
            if raw_value.tag == q::JS_TAG_EXCEPTION.into() {
              return Err(String::from("get object property error"));
            }
            let key_value = unsafe { q::JS_AtomToString(c_ctx, (*prop).atom) };
            if key_value.tag == q::JS_TAG_EXCEPTION.into() {
              return Err(String::from("get object property error"));
            }
            let k = RawJsValue::deserialize_value(key_value, c_ctx)?.to_string();
            AutoDropJSValue::drop_js_value(key_value,c_ctx);
            map.insert(k, AutoDropJSValue::new(raw_value, c_ctx));
            
            #[cfg(unix)]
            unsafe { q::JS_FreeAtom(c_ctx, (*prop).atom); }
          }
          unsafe { q::js_free(c_ctx, properties as *mut std::ffi::c_void); }
          Ok(JSValue::Object(map))
        }
       
      },
      q::JS_TAG_MODULE => {Ok(JSValue::Module())},
      _ => Ok(JSValue::Exception)
    }
  }

  pub fn deserialize_value(c_js_value:q::JSValue,c_ctx:*mut q::JSContext) -> Result<JSValue,String> {
    Self::deserialize_value_(c_js_value,c_ctx,false)
  }

  pub fn is_exception(&self) -> bool {
    self.0.tag == q::JS_TAG_EXCEPTION.into()
  }

  pub fn is_int(&self) -> bool {
    self.0.tag == q::JS_TAG_INT.into()
  }

  pub fn is_null(&self) -> bool {
    self.0.tag == q::JS_TAG_NULL.into()
  }

  pub fn is_error(&self,ctx:*mut q::JSContext) -> bool {
    unsafe {q::JS_IsError(ctx,self.0) > 0 }
  }

  pub fn get_property_str(&self,str:&str,ctx:*mut q::JSContext) -> RawJsValue {
    let c_str = CString::new(str).unwrap();
    let raw_v = unsafe {q::JS_GetPropertyStr(ctx,self.0,c_str.as_ptr())};
    RawJsValue(raw_v)
  }

  pub fn val_null() -> q::JSValue {
    q::JSValue {
            u: q::JSValueUnion { int32: 0 },
            tag: q::JS_TAG_NULL as i64
    }
  }

  pub fn val_undefined() -> q::JSValue {
    q::JSValue {
            u: q::JSValueUnion { int32: 0 },
            tag: q::JS_TAG_UNDEFINED as i64
    }
  }


  pub fn val_exception() -> q::JSValue {
    q::JSValue {
            u: q::JSValueUnion { int32: 0 },
            tag: q::JS_TAG_EXCEPTION as i64
    }
  }

  pub fn val_i32(num:i32) -> q::JSValue {
    q::JSValue {
            u: q::JSValueUnion { int32: num  },
            tag: q::JS_TAG_INT as i64
    }
  }

  pub fn val_bool(b:bool) -> q::JSValue {
    q::JSValue {
            u: q::JSValueUnion { int32: (if b {1} else {0}) },
            tag: q::JS_TAG_BOOL as i64
    }
  }

  pub fn val_f64(num:f64) -> q::JSValue {
    q::JSValue {
            u: q::JSValueUnion { float64: num },
            tag: q::JS_TAG_FLOAT64 as i64
    }
  }

  pub fn val_string(str:&String,ctx:*mut q::JSContext) -> q::JSValue {
    let qval = unsafe { q::JS_NewStringLen(ctx, str.as_ptr() as *const i8, str.len()) };
    qval
  }

  pub fn val_array_buffer(buf:Vec<u8>,ctx:*mut q::JSContext) -> q::JSValue {
     unsafe {
       q::JS_NewArrayBufferCopy(ctx,buf.as_ptr(), buf.len())
     }
  }


  pub fn ref_count(&self) -> i32 {
    if self.tag() < 0 {
      unsafe {
        let ptr =  self.0.u.ptr as *mut q::JSRefCountHeader;
        let pref: &mut q::JSRefCountHeader = &mut *ptr;
        return pref.ref_count as i32;
      };
    };
    -1
  }

  pub fn add_ref_count(&self,num:i32) -> bool {
    if self.tag() < 0 {
      unsafe {
        let ptr =  self.0.u.ptr as *mut q::JSRefCountHeader;
        let pref: &mut q::JSRefCountHeader = &mut *ptr;
        pref.ref_count += num;
      };
      return true;
    };
    return false;
  }
}




pub enum JSValue {
  Undefined,
  Null,
  Int(i32),
  Bool(bool),
  Float(f64),
  String(String),
  Array(Vec<JSValue>),
  ArrayRaw(Vec<AutoDropJSValue>),
  Object(HashMap<String, AutoDropJSValue>),
  Function(AutoDropJSValue),
  Module(),
  Exception
}


impl JSValue {
  pub fn to_string(&self) -> String { 
    self._to_string(false)
  }

  pub fn display_string(&self) -> String {
     self._to_string(true)
  }

  fn _to_string(&self,is_display:bool) -> String {
    match self {
      JSValue::Int(num) => num.to_string(),
      JSValue::Bool(b) => b.to_string(),
      JSValue::Float(fnum) => fnum.to_string(),
      JSValue::String(str) => {
        if is_display == false {
          return String::from(str)
        }
        let mut new_str = String::default();
        new_str.push('"');
        new_str.push_str(str);
        new_str.push('"');
        new_str
      },
      JSValue::Array(arr) => {
        let mut ret_string = String::from("[");
        for i in 0..(arr.len()) {
          let s = arr.get(i).unwrap().to_string();
          ret_string.push_str(&s);
          if i != (arr.len() - 1) {
            ret_string.push(',');
          }
        }
        ret_string.push(']');
        ret_string
      },
      JSValue::ArrayRaw(_) => String::from("[RawArray]"),
      JSValue::Object(obj_map) => {
        let mut i = 0;
        let mut ret_string = String::from("{");
        for (k,_v) in obj_map {
          ret_string.push_str(k);
          ret_string.push(':');
          ret_string.push_str("[Object]");
          i = i + 1;
          if i < obj_map.len() {
             ret_string.push_str(",");
          }
        }
        ret_string.push('}');
        ret_string
      },
      JSValue::Exception  => String::from("[Exception]"),
      JSValue::Null => String::from("null"),
      JSValue::Undefined => String::from("undefined"),
      JSValue::Function(_) => String::from("function"),
      JSValue::Module() => String::from("Module")
    }
  }

  pub fn as_bool(&self) -> Option<bool> {
    match self {
      JSValue::Bool(b) => Some(*b),
      _ => None
    }
  }

  pub fn into_bool(self) -> Option<bool> {
    match self {
      JSValue::Bool(b) => Some(b),
      _ => None
    }
  }

  pub fn as_object(&self) -> Option<&HashMap<String,AutoDropJSValue>> {
    match self {
      JSValue::Object(map) => Some(map),
      _ => None
    }
  }

  pub fn into_object(self) -> Option<HashMap<String,AutoDropJSValue>> {
    match self {
      JSValue::Object(map) => Some(map),
      _ => None
    }
  }

  pub fn object_get_number(&self,k:&str,ctx:*mut q::JSContext) -> Option<f64> {
    let val = self.as_object().and_then(|o|o.get(&String::from(k)))
                              .and_then(|js_val| RawJsValue::deserialize_value(js_val.inner().0,ctx).ok());
    val.and_then(|v|v.as_number())
  }

  pub fn as_float64(&self) -> Option<f64> {
    match self {
      JSValue::Float(w) => Some(*w),
      _ => None
    }
  }

  pub fn as_array_number(&self) -> Option<Vec<f64>> {
    if let Some(arr) = self.as_array() {
      let mut ret_numbers:Vec<f64> = vec![];
      for val in arr {
        if let Some(num) = val.as_number() {
          ret_numbers.push(num);
        } else {
          return None;
        }
      }
      return Some(ret_numbers);
    }
    None
  }

  pub fn as_array_int(&self) -> Option<Vec<i32>> {
    if let Some(arr) = self.as_array() {
      let mut ret_numbers:Vec<i32> = vec![];
      for val in arr {
        if let Some(num) = val.as_int() {
          ret_numbers.push(num);
        } else {
          return None;
        }
      }
      return Some(ret_numbers);
    }
    None
  }

  pub fn as_int(&self) -> Option<i32> {
    match self {
      JSValue::Int(n) => Some(*n),
      _ => None
    }
  }

  pub fn into_string(self) -> Option<String> {
    match self {
      JSValue::String(s) => Some(s),
      _ => None
    }
  }

  pub fn as_number(&self) -> Option<f64> {
    match self {
      JSValue::Int(n) => Some(*n as f64),
      JSValue::Float(n) => Some(*n),
      _ => None
    }
  }

  pub fn as_array(&self) -> Option<&Vec<JSValue>> {
    match self {
      JSValue::Array(w) => Some(w),
      _ => None
    }
  }

  pub fn into_array_raw(self) -> Option<Vec<AutoDropJSValue>> {
    match self {
      JSValue::ArrayRaw(w) => Some(w),
      _ => None
    }
  }

  pub fn into_array(self) -> Option<Vec<JSValue>> {
    match self {
      JSValue::Array(w) => Some(w),
      _ => None
    }
  }

  pub fn into_func(self) -> Option<AutoDropJSValue> {
    match self {
      JSValue::Function(v) => Some(v),
      _ => None
    }
  }

  pub fn to_c_value(&self,ctx:*mut q::JSContext) -> q::JSValue {
    match self {
      JSValue::Int(num) => RawJsValue::val_i32(*num),
      JSValue::Bool(b) => RawJsValue::val_bool(*b),
      JSValue::Float(num) => RawJsValue::val_f64(*num),
      JSValue::String(str) => RawJsValue::val_string(str, ctx),
      JSValue::Array(arr) => {
        let js_arr = unsafe { q::JS_NewArray(ctx) };
        for (index, value) in arr.into_iter().enumerate() {
          let val = value.to_c_value(ctx);
          unsafe {
            q::JS_DefinePropertyValueUint32(
                        ctx,
                        js_arr,
                        index as u32,
                        val,
                        q::JS_PROP_C_W_E as i32)
          };
        }
        js_arr
      },
      JSValue::Object(obj_map) => {
        let obj = unsafe { q::JS_NewObject(ctx) };
         for (key, value) in obj_map {
          let c_key = CString::new(key.as_str()).unwrap();
          let qvalue = value.inner().0;
          unsafe {
            q::JS_DefinePropertyValueStr(
                        ctx,
                        obj,
                        c_key.as_ptr(),
                        qvalue,
                        q::JS_PROP_C_W_E as i32);
          };
         }
        obj
      },
      JSValue::Undefined => RawJsValue::val_undefined(),
      _ => RawJsValue::val_null()
    }
    
  }
}

